package net.wsnetwork.jawa.common;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLServerSocketFactory;

public class Server {
    private static final int ACCEPT_BACKLOG = 1000;
    private static final int PROCESS_QUEUE_SIZE = 1000;
    private static final int THREADS = 1000;

    private static final Logger log = Logger.getLogger(Server.class.getCanonicalName());

    private final ServerSocket socket;
    private final Thread acceptThread;
    private final ConnectionHandler connectionHandler;
    private boolean running;

    public Server(int port, InetAddress bindAddr, ConnectionHandler connectionHandler,
            boolean useTLS)
    throws IOException {
        if (useTLS) {
            ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
            socket = factory.createServerSocket(port, ACCEPT_BACKLOG, bindAddr);
        } else {
            socket = new ServerSocket(port, ACCEPT_BACKLOG, bindAddr);
        }
        acceptThread = new Thread(new SocketAcceptRunnable());
        this.connectionHandler = connectionHandler;
    }

    public Server(int port, ConnectionHandler connectionHandler,
            boolean useTLS) throws IOException {
        if (useTLS) {
            ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
            socket = factory.createServerSocket(port, ACCEPT_BACKLOG);
        } else {
            socket = new ServerSocket(port, ACCEPT_BACKLOG);
        }
        acceptThread = new Thread(new SocketAcceptRunnable());
        this.connectionHandler = connectionHandler;
    }

    public void start() {
        running = true;
        acceptThread.start();
    }

    public void stop() throws IOException {
        running = false;
        acceptThread.interrupt();
        socket.close();
    }

    private class SocketAcceptRunnable implements Runnable {
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(PROCESS_QUEUE_SIZE);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(THREADS, THREADS, 60,
                TimeUnit.SECONDS, workQueue);

        @Override public void run() {
            while (running) {
                try {
                    executor.submit(new ConnectionHandlerRunnable(socket.accept()));
                } catch (SocketTimeoutException e) {
                    // Accept times out periodically, do nothing
                } catch (IOException e) {
                    StringWriter result = new StringWriter();
                    PrintWriter printWriter = new PrintWriter(result);
                    e.printStackTrace(printWriter);
                    log.log(Level.WARNING, "Error accepting connection: " + result.toString());
                }
            }
        }
    }

    private class ConnectionHandlerRunnable implements Runnable {
        private final Socket connection;

        public ConnectionHandlerRunnable(Socket connection) {
            this.connection = connection;
        }

        @Override public void run() {
            try {
                socket.setSoTimeout(10000);
                InputStream in = connection.getInputStream();
                OutputStream out = connection.getOutputStream();
                while (true) {
                    try {
                        boolean keepOpen = connectionHandler.handleConnection(in, out);
                        if (!keepOpen) { return; }
                    } catch (SocketException e) {
                        // Connection was closed
                        log.log(Level.INFO, "Connection closed.");
                        return;
                    } catch (SocketTimeoutException e) {
                        // Connection timed out
                        log.log(Level.INFO, "Connection timed out.");
                        return;
                    } catch (IOException e) {
                        // Misc. I/O exception
                        StringWriter result = new StringWriter();
                        PrintWriter printWriter = new PrintWriter(result);
                        e.printStackTrace(printWriter);
                        log.log(Level.WARNING, "I/O error while processing connection: "
                                + result.toString());
                        return;
                    } catch (ConnectionProcessingException e) {
                        StringWriter result = new StringWriter();
                        PrintWriter printWriter = new PrintWriter(result);
                        e.printStackTrace(printWriter);
                        log.log(Level.WARNING, "Error processing connection: "
                                + result.toString());
                        return;
                    }
                }
            } catch (Exception e) {
                StringWriter result = new StringWriter();
                PrintWriter printWriter = new PrintWriter(result);
                e.printStackTrace(printWriter);
                log.log(Level.WARNING, "Error opening connection I/O streams: "
                        + result.toString());
            } finally {
                try {
                    connection.close();
                } catch (IOException e) {
                    StringWriter result = new StringWriter();
                    PrintWriter printWriter = new PrintWriter(result);
                    e.printStackTrace(printWriter);
                    log.log(Level.WARNING, "Error closing connection: " + result.toString());
                }
            }
        }
    }
}
